package com.itbupt.config;

import com.itbupt.types.annotations.DCCValue;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author dingchang
 * @Date 2025/4/13 22:32
 * @Description: 基于 Zookeeper 的配置中心实现原理
 * 用于动态更新 Spring 容器中带有 @DCCValue 注解的字段值
 */

@Slf4j
@Configuration
public class DCCValueBeanFactory implements BeanPostProcessor {

    // Zookeeper 中配置节点的路径
    private static final String BASE_CONFIG_PATH = "/big-market-dcc";
    // 配置中心的根节点路径，所有配置信息都存储在该路径下
    private static final String BASE_CONFIG_PATH_CONFIG = BASE_CONFIG_PATH + "/config";

    // CuratorFramework 的实例，用于与 Zookeeper 进行交互。
    private final CuratorFramework client;

    // 用于存储配置路径与对应的 Bean 的映射关系
    // 当 Zookeeper 中的配置发生变化时，可以根据这个映射找到对应的 Bean 并更新其字段值。
    private final Map<String, Object> dccObjGroup = new HashMap<>();


    public DCCValueBeanFactory(CuratorFramework client) throws Exception {
        this.client = client;

        // 节点判断 - 检查并创建 ZooKeeper 根节点
        if(null == client.checkExists().forPath(BASE_CONFIG_PATH_CONFIG)) {
            client.create().creatingParentsIfNeeded().forPath(BASE_CONFIG_PATH_CONFIG);
            log.info("DCC 节点监听 base node {} not absent create new done!", BASE_CONFIG_PATH_CONFIG);
        }

        // 创建并启动监听器
        CuratorCache curatorCache = CuratorCache.build(client, BASE_CONFIG_PATH_CONFIG);
        curatorCache.start();

        // 监听器的回调函数， 当 Zookeeper 中的节点发生变化时，会触发回调函数
        // 在回调函数中，根据节点的变化类型，更新对应的 Bean 字段值
        curatorCache.listenable().addListener((type, oldData, data) -> {
            switch (type) {
                case NODE_CHANGED:
                    String dccValuePath = data.getPath();
                    Object objBean = dccObjGroup.get(dccValuePath);
                    if (null == objBean) {
                        return;
                    }
                    try {
                        Class<?> objBeanClass = objBean.getClass();
                        // 检查bean是否是代理对象
                        if(AopUtils.isAopProxy(objBean)) {
                            objBean = AopUtils.getTargetClass(objBean);
                            // objBeanClass = AopProxyUtils.ultimateTargetClass(objBean);
                        }

                        // 1. getDeclaredField 方法用于获取指定类中声明的所有字段，包括私有字段、受保护字段和公共字段。
                        // 2. getField 方法用于获取指定类中的公共字段，即只能获取到公共访问修饰符（public）的字段。
                        Field field = objBean.getClass().getDeclaredField(dccValuePath.substring(dccValuePath.lastIndexOf("/") + 1));
                        field.setAccessible(true);
                        field.set(objBean, new String(data.getData()));
                        field.setAccessible(false);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    break;
                default:
                    break;
            }
        });
    }

    // 在 Spring 容器初始化 Bean 之后，对带有 @DCCValue 注解的字段进行处理。
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<?> targetBeanClass = bean.getClass();
        Object targetBeanObject = bean;
        if (AopUtils.isAopProxy(bean)) {
            targetBeanClass = AopUtils.getTargetClass(bean);
            targetBeanObject = AopProxyUtils.getSingletonTarget(bean);
        }

        // 获取Bean的所有字段
        Field[] fields = targetBeanClass.getDeclaredFields();
        for (Field field : fields) {

            // 检查字段是否带有 @DCCValue 注解
            if (!field.isAnnotationPresent(DCCValue.class)) {
                continue;
            }

            // 获取注解的值 - 字符串
            // 格式为 key:defaultValue 其中，key 是 Zookeeper 中的配置键，defaultValue 是默认值。
            DCCValue dccValue = field.getAnnotation(DCCValue.class);

            String value = dccValue.value();
            if (StringUtils.isBlank(value)) {
                throw new RuntimeException(field.getName() + " @DCCValue is not config value config case 「isSwitch/isSwitch:1」");
            }

            String[] splits = value.split(":");
            String key = splits[0];
            String defaultValue = splits.length == 2 ? splits[1] : null;

            try {
                // 判断当前节点是否存在，不存在则创建出 Zookeeper 节点
                String keyPath = BASE_CONFIG_PATH_CONFIG.concat("/").concat(key);
                if (null == client.checkExists().forPath(keyPath)) {
                    client.create().creatingParentsIfNeeded().forPath(keyPath);

                    // 使用注解中的默认值
                    if (StringUtils.isNotBlank(defaultValue)) {
                        field.setAccessible(true);
                        field.set(targetBeanObject, defaultValue);
                        field.setAccessible(false);
                    }
                    log.info("DCC 节点监听 创建节点 {}", keyPath);
                } else {
                    // 获取节点的值
                    String configValue = new String(client.getData().forPath(keyPath));
                    // 通过反射将该值设置到字段中。
                    if (StringUtils.isNotBlank(configValue)) {
                        field.setAccessible(true);
                        field.set(targetBeanObject, configValue);
                        field.setAccessible(false);
                        log.info("DCC 节点监听 设置配置 {} {} {}", keyPath, field.getName(), configValue);
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            // 将配置路径与对应的 Bean 添加到 dccObjGroup 中
            // 以便在 Zookeeper 节点发生变化时，能够找到对应的 Bean 并更新其字段值
            dccObjGroup.put(BASE_CONFIG_PATH_CONFIG.concat("/").concat(key), targetBeanObject);
        }
        return bean;
    }


}
